import datetime
import os.path
import time
from typing import Union, Callable, Set, List, Iterable

from scripts.Log import Log
from scripts.actions.Context import Context
from scripts.device.AdbDevice import AdbDevice
from scripts.device.AdbShell import AdbShell
from scripts.device.FastbootDevice import FastbootDevice
from scripts.util.Util import Util


class Action(object):
    _registered_keys: Set[str] = set()

    @staticmethod
    def register(key: str) -> str:
        if key in Action._registered_keys:
            raise Exception('Action key already registered')

        Action._registered_keys.add(key)
        return key

    def __init__(self, key: str, display_name: str, context: Context, create_data_dir: bool = True):
        self._key = key
        self._display_name = display_name
        self._context = context
        self._create_data_dir = create_data_dir
        self._begin_time: Union[None, datetime.datetime] = None

    def key(self) -> str:
        return self._key

    def display_name(self) -> str:
        return self._display_name

    def context(self) -> Context:
        return self._context

    def data_dir(self) -> str:
        return os.path.join(self.context().host_dir(), self.key())

    def sn(self) -> str:
        return self._context.sn()

    def device_temp_dir(self) -> str:
        return self._context.device_temp_dir()

    def android_version(self) -> Union[None, int]:
        return self._context.android_version()

    def set_android_version(self, version: int):
        self._context.set_android_version(version)

    def device_model(self) -> Union[None, str]:
        return self._context.device_model()

    def set_device_model(self, model: str):
        self._context.set_device_model(model)

    def duration_time(self) -> datetime.timedelta:
        return datetime.datetime.now() - self._begin_time

    def duration_time_str(self) -> str:
        delta = self.duration_time()
        minutes = delta.seconds // 60
        seconds = delta.seconds % 60
        return '%02d:%02d.%03d' % (minutes, seconds, delta.microseconds // 1000)

    def wait_for_device(self) -> AdbDevice:
        device = AdbDevice.wait_for_device(self.sn())
        Log.d('got adb device: %s' % device.sn())
        return device

    def wait_for_device_rooted(self) -> Union[None, AdbDevice]:
        device = AdbDevice.wait_for_device(self.sn())
        device.adb_root()
        device = AdbDevice.wait_for_device(self.sn())
        with device.shell() as shell:
            rooted = shell.is_rooted()
        if rooted:
            Log.d('got rooted device: %s' % device.sn())
            return device
        else:
            Log.d('root device failed: %s' % device.sn())
            return None

    def wait_for_fastboot_device(self) -> FastbootDevice:
        fastboot_device = FastbootDevice.wait_for_device(self.sn())
        Log.d('got fastboot device: %s' % fastboot_device.sn())
        return fastboot_device

    def backup(self, progress: Union[None, Callable[[int, int, str, int, int, str], None]] = None):
        Log.i('>> Backup: %s' % self.display_name())

        self._begin_time = datetime.datetime.now()

        if self._create_data_dir:
            os.makedirs(self.data_dir(), exist_ok=True)

        self._on_backup(progress)

    def restore(self, progress: Union[None, Callable[[int, int, str, int, int, str], None]] = None):
        Log.i('>> Restore: %s' % self.display_name())

        self._begin_time = datetime.datetime.now()

        if not self._create_data_dir or os.path.exists(self.data_dir()):
            self._on_restore(progress)
        else:
            Log.e('Data dir not found. You should backup first!!!')

    def _on_backup(self, progress: Union[None, Callable[[int, int, str, int, int, str], None]]):
        pass

    def _on_restore(self, progress: Union[None, Callable[[int, int, str, int, int, str], None]]):
        pass

    @staticmethod
    def wait_for_filesystem_ready(users: Union[int, Iterable[int]], shell: AdbShell):
        if isinstance(users, int):
            shell.wait_for_filesystem_ready(users)
            Log.d('[%d] filesystem ready' % users)
        elif isinstance(users, Iterable):
            for user in users:
                Action.wait_for_filesystem_ready(user, shell)

    def wait_for_device_tmp_dir_ready(self, shell: AdbShell):
        shell.wait_for_path_ready(self.device_temp_dir())
        Log.d('tmp path [%s] ready' % self.device_temp_dir())

    def wait_for_boot_up_ready(self) -> AdbDevice:
        device = self.wait_for_device_rooted()
        with device.shell() as shell:
            shell.wait_for_boot_complete()
            shell.wait_for_service([
                AdbShell.SERVICE_WINDOW,
                AdbShell.SERVICE_PACKAGE,
                AdbShell.SERVICE_ACTIVITY,
                AdbShell.SERVICE_SETTINGS,
                AdbShell.SERVICE_INPUT
            ])
        time.sleep(10)  # sleep 10 seconds to wait for everything ready
        return device

    @staticmethod
    def start_users(users: Union[int, Iterable[int]], shell: AdbShell):
        if isinstance(users, int):
            Log.d('start user: %d' % users)
            while shell.am_is_user_stopped(users):
                shell.am_start_user(users)
        elif isinstance(users, Iterable):
            for user in users:
                Action.start_users(user, shell)

    @staticmethod
    def stop_users(users: Union[int, Iterable[int]], shell: AdbShell):
        if isinstance(users, int):
            if users != 0 and users != shell.am_get_current_user():
                Log.d('stop user: %d' % users)
                while not shell.am_is_user_stopped(users):
                    shell.am_stop_user(users)
        elif isinstance(users, Iterable):
            for user in users:
                Action.stop_users(user, shell)

    @staticmethod
    def switch_user_await(user: int, shell: AdbShell):
        shell.am_switch_user(user)
        while shell.am_get_current_user() != user:
            Log.d('switch user not finished. wait for another second')
            time.sleep(1)

    @staticmethod
    def pull_file(device: AdbDevice, file_list, shell: Union[None, AdbShell] = None):
        local_shell: Union[None, AdbShell] = None
        if shell is None:
            local_shell = device.shell()
            local_shell.start()
            shell = local_shell

        if isinstance(file_list, Action.FileTransferItem):
            if not os.path.exists(file_list.host_dir):
                os.makedirs(file_list.host_dir, exist_ok=True)
            if file_list.compress:
                result, reason = shell.tar_compress(file_list.device_path,
                                                    file_list.device_tmp_path,
                                                    file_list.compress_include,
                                                    file_list.compress_exclude)  # compress to a single file
                if not result and reason == AdbShell.TAR_ERROR_REASON_EMPTY:
                    Log.d('got empty tar, ignore it.')
                else:
                    device.pull(file_list.device_tmp_path, file_list.host_path)  # pull compressed pkg to local
                    shell.rm(file_list.device_tmp_path)  # remove temp file
            else:
                device.pull(file_list.device_path, file_list.host_path)
        elif isinstance(file_list, Iterable):
            for item in file_list:
                Action.pull_file(device, item, shell)
        else:
            Log.e('unknown type of file_list: %s' % type(file_list))

        if local_shell:
            local_shell.exit()

    @staticmethod
    def push_file(device: AdbDevice, file_list, shell: Union[None, AdbShell] = None):
        local_shell: Union[None, AdbShell] = None
        if shell is None:
            local_shell = device.shell()
            local_shell.start()
            shell = local_shell

        if isinstance(file_list, Action.FileTransferItem):
            if file_list.clear_copy:
                shell.rm(file_list.device_path)  # remove old file before restore
            if file_list.compress:
                device.push(file_list.host_path, file_list.device_tmp_path)  # push tar to device
                shell.tar_extract(file_list.device_tmp_path, file_list.device_dir)  # extract tar to target
                shell.rm(file_list.device_tmp_path)  # remove temp file
            else:
                if os.path.isdir(file_list.host_path) and shell.path_exists(file_list.device_path):
                    # host is dir and device dir exists
                    device.push(file_list.host_path, os.path.dirname(file_list.device_path))
                else:
                    device.push(file_list.host_path, file_list.device_path)

                # create empty dirs
                Util.create_device_dirs(file_list.host_path, file_list.device_path, shell=shell, empty_only=True)
        elif isinstance(file_list, Iterable):
            for item in file_list:
                Action.push_file(device, item, shell)
        else:
            Log.e('unknown type of file_list: %s' % type(file_list))

        if local_shell:
            local_shell.exit()

    @staticmethod
    def device_path_to_host_path(device_path: str, host_dir: str, compress: bool = False) -> str:
        host_name = device_path.replace('/', '-')
        if compress:
            return os.path.join(host_dir, '%s.tar.gz' % host_name)
        else:
            return os.path.join(host_dir, host_name)

    @staticmethod
    def host_path_to_device_path(host_path: str, compress: bool = False) -> str:
        host_file_name = os.path.basename(host_path)
        if compress:
            return host_file_name[0:-7].replace('-', '/')
        else:
            return host_file_name.replace('-', '/')

    def backup_path(self, device_path: str, host_dir: str,
                    device: AdbDevice, shell: AdbShell,
                    compress: bool = False,
                    compress_include: Union[None, List[str]] = None, compress_exclude: Union[None, List[str]] = None):
        # ignore paths that not necessary to do backup
        if compress and compress_include is not None:
            compress_include = list(filter(lambda p: shell.path_exists('%s/%s' % (device_path, p)), compress_include))
            if len(compress_include) == 0:
                Log.d('ignore: %s' % device_path)
                return

        # generate host_path & device_tmp_path
        host_path = Action.device_path_to_host_path(device_path, host_dir, compress)
        if compress:
            device_tmp_path = '%s/%s.tar.gz' % (self.device_temp_dir(), os.path.basename(host_path))
        else:
            device_tmp_path = None

        # pull file from device
        item = Action.FileTransferItem(device_path=device_path,
                                       host_path=host_path,
                                       device_tmp_path=device_tmp_path,
                                       compress=compress,
                                       compress_include=compress_include,
                                       compress_exclude=compress_exclude)
        Action.pull_file(device, item, shell=shell)

    def restore_path(self, host_path: str,
                     device: AdbDevice, shell: AdbShell,
                     compress: bool = False, clear_copy: bool = False):
        # generate device_path & device_tmp_path
        device_path = Action.host_path_to_device_path(host_path, compress)
        if compress:
            device_tmp_path = '%s/%s' % (self.device_temp_dir(), os.path.basename(host_path))
        else:
            device_tmp_path = None

        # push file to device
        item = Action.FileTransferItem(device_path=device_path,
                                       host_path=host_path,
                                       device_tmp_path=device_tmp_path,
                                       compress=compress,
                                       clear_copy=clear_copy)
        Action.push_file(device, item, shell=shell)

    class FileTransferItem(object):
        def __init__(self, device_path: str, host_path: str, device_tmp_path: Union[None, str] = None,
                     compress: bool = False,
                     compress_include: Union[None, List[str]] = None, compress_exclude: Union[None, List[str]] = None,
                     clear_copy: bool = False):
            self.device_path = device_path
            self.device_dir = os.path.dirname(self.device_path)
            self.device_tmp_path = device_tmp_path
            self.device_tmp_dir = os.path.dirname(self.device_tmp_path) if self.device_tmp_path is not None else None
            self.host_path = host_path
            self.host_dir = os.path.dirname(self.host_path)
            self.compress = compress
            self.compress_include = compress_include
            self.compress_exclude = compress_exclude
            self.clear_copy = clear_copy
